package repo

import (
	"bytes"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"github.com/pkg/errors"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

type HashKey []byte

func (m HashKey) String() string {
	return hex.EncodeToString(m)
}

func (m HashKey) Cut() [3]string {
	var out [3]string
	key := hex.EncodeToString(m)
	if len(key) < 5 {
		panic("hash key too small")
	}

	out[0] = key[:2]
	out[1] = key[2:4]
	out[2] = key[4:]
	return out
}

func HashKeyFromString(s string) (HashKey, error) {
	h, err := hex.DecodeString(s)
	if err != nil {
		return nil, errors.Wrap(err, "hex decode failed")
	}

	return h, nil
}

/*
Add 添加文件到存储，必须事务完成，若添加失败，则Get/Exist 必须失败
*/
type FileStorage interface {
	Add([]byte, HashKey) error
	Get(HashKey) ([]byte, error)
	Del(HashKey) error
}

type FileIoStorage interface {
	AddFrom(io.Reader, HashKey) error
	WriteTo(HashKey) (io.Writer, error)
}

var glStorage FileStorage = newFsStorage("./storage")

func Instance() FileStorage {
	return glStorage
}

type FsStorage struct {
	basePath string
}

func (m *FsStorage) AddFrom(r io.Reader, key HashKey) error {
	fullPath := m.getFullPath(key)

	// 文件路径是否存在，否则应 mkdir
	filePath := m.getFilePath(key)
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		// path/to/whatever does not exist
		err = os.MkdirAll(filePath, 0666)
		if err != nil {
			return errors.Wrap(err, fmt.Sprintf("mkdir %s failed", filePath))
		}
	}

	dst, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		return errors.Wrap(err, "open file failed")
	}
	_, err = io.Copy(dst, r)
	if err != nil {
		// remove file
		removeErr := os.Remove(fullPath)
		if removeErr != nil {
			// error, but ignored.
			println("write file failed, remove it failed too!")
		}
		return errors.Wrap(err, fmt.Sprintf("create %s failed", fullPath))
	}

	return nil
}

func (m *FsStorage) WriteTo(HashKey) (io.Writer, error) {
	panic("implement me")
}

func newFsStorage(base string) FileStorage {
	return &FsStorage{
		basePath: base,
	}
}

func HashData(d []byte) HashKey {
	h := sha1.New()

	// never return error
	_, _ = h.Write(d)
	return h.Sum(nil)
}

func HashStringList(sl []string) HashKey {
	h := sha1.New()
	for _, w := range sl {
		_, _ = h.Write([]byte(w))
	}

	return h.Sum(nil)
}

func (m *FsStorage) getFullPath(k HashKey) string {
	fields := k.Cut()
	return filepath.Join(m.basePath, fields[0], fields[1], fields[2])
}

func (m *FsStorage) getFilePath(k HashKey) string {
	fields := k.Cut()
	return filepath.Join(m.basePath, fields[0], fields[1])
}

func (m *FsStorage) Add(d []byte, key HashKey) error {
	return m.AddFrom(bytes.NewReader(d), key)
}

func (m *FsStorage) Get(k HashKey) ([]byte, error) {
	fullPath := m.getFullPath(k)
	return ioutil.ReadFile(fullPath)
}

func (m *FsStorage) Del(k HashKey) error {
	return os.Remove(m.getFullPath(k))
}
